About
Spectral Teletype is an interactive audio-visual piece that turns messages into melodies. Participants are invited to transmit messages from their smartphones, and the messages are used to simultaneously display and play a synthetic representation of the text in a scrolling audio spectrum.

Melodies are created from strings of characters by printing the characters into the audio spectrum. In order to sonically print the characters, an oscillator bank is configured as an aural dot matrix printer, and the characters are divided into 32 by 32 pixel grids. The oscillator bank plays these pixels as sine waves in harmonic relationships, creating notes that represent the shapes of the characters in the audio spectrum.

The pitch of each character is selected from a major eleventh chord, with more frequently used characters the toward the root of the chord. Additionally, the chord is modulated between scale degrees I, IV, and V based on the first character of each word.

This work was created with custom software written in the Pure Data, HTML, PHP, and C programming languages.

Spectral Teletype was exhibited in 2015 during the Make Music San Diego event at the San Diego Art Institute, and in the 2015 Ephemeral Objects show at the San Diego Art Institute.

Video


Photos

Code
A couple of custom Pure Data objects were used to realize this piece. One is an oscillator bank, named teletype~, that prints the characters as sound. The other object is a fifo buffer for ascii messages, named asciibuf, that allows strings of characters to be stored en masse while individual characters are printed at regular intervals. The C code for these objects is shown below.

teletype~.c
//------------------------------------------------------------------------------
//  Spectral ASCII Printer
//
//  teletype~.c
//
//  Spectrally Prints ASCII Characters Using Harmonically Related Oscillators
//
//  Created by Cooper Baker on 05/10/15.
//  Copyright (c) 2015 Cooper Baker. All rights reserved.
//------------------------------------------------------------------------------


//------------------------------------------------------------------------------
// headers
//------------------------------------------------------------------------------

// main header for pd
#include "m_pd.h"

// teletype~ font file
#include "teletype~.font"

// utility header for Pd Objects project
#include "utility.h"

// disable compiler warnings on windows
#ifdef NT
#pragma warning( disable : 4244 )
#pragma warning( disable : 4305 )
#endif

// 2^13 point wavetable
#define WAVETABLE_SIZE 8192
#define WAVETABLE_MASK 8191

// number of partials
#define OSCILLATORS 32


//------------------------------------------------------------------------------
// teletype_class - pointer to this object's definition
//------------------------------------------------------------------------------
static t_class* teletype_class;


//------------------------------------------------------------------------------
// teletype - data structure holding this object's data
//------------------------------------------------------------------------------
typedef struct teletype
{
    // this object - must always be first variable in struct
    t_object object;
   
    // needed for CLASS_MAINSIGNALIN macro call in teletype_tilde_setup
    t_float inlet_1;
   
    // fundamental frequency
    t_float freq;
   
    // frequency smoothing variable
    t_float freq_smooth;
   
    // pointer to the wavetable
    t_float* wavetable;

    // amplitude of each oscillator
    t_float* amp;

    // smoothed amplitude of each oscillator
    t_float* amp_smooth;

    // amplitude smoothing increments
    t_float* amp_smooth_inc;
   
    // oscbank phase
    t_float* phase;
   
    // oscbank phase increment
    t_float* phase_inc;
   
    // waveform synthesis buffers
    t_float* raw;
    t_float* saw;
   
    // size of signal vector
    t_float vector_bytes;
   
    // sample rate of object
    t_float sample_rate;
   
    // milliseconds per pixel
    t_float pixel_msec;
   
    // font bitmap of characters
    t_float* fontmap;
   
    // counter for samples per pixel
    t_float pixel_samps;

    // x pixel location within character
    t_int x_pixel;
   
    // character offset within fontmap
    t_int char_offset;
   
    // print character flag
    t_int print_char;
   
} t_teletype;


//------------------------------------------------------------------------------
// function prototypes
//------------------------------------------------------------------------------
static void   teletype_float       ( t_teletype* object, t_floatarg number );
static void   teletype_message     ( t_teletype* object, t_symbol* selector, t_int items, t_atom* list );
static t_int* teletype_perform     ( t_int* io );
static void   teletype_dsp         ( t_teletype* object, t_signal **sig );
static void*  teletype_new         ( t_symbol *s, t_int argc, t_atom *argv );
static void   teletype_free        ( t_teletype* object );
void          teletype_tilde_setup ( void );


//------------------------------------------------------------------------------
// teletype_float - handles float input
//------------------------------------------------------------------------------
static void teletype_float( t_teletype* object, t_floatarg number )
{
    // ignore out of range numbers ( range: ascii 32 to 94 )
    if( ( number < 32 ) || ( number > 126 ) )
    {
        return;
    }
   
    // translate ascii to local
    number -= 32;
   
    // reset character position
    object->pixel_samps = 0;
    object->x_pixel     = 0;
   
    // calculate character offset
    object->char_offset = number * fontmap_x_px * fontmap_x_px;
   
    // set print character flag
    object->print_char = TRUE;
}


//------------------------------------------------------------------------------
// teletype_message - handles input messages
//------------------------------------------------------------------------------
static void teletype_message( t_teletype* object, t_symbol* selector, t_int items, t_atom* list )
{
    char* message = selector->s_name;
   
    if( StringMatch( message, "msec" ) )
    {
        if( items < 1 )
        {
            pd_error( object, "teletype~: no msec argument" );
            return;
        }
       
        if( list[ 0 ].a_type == A_FLOAT )
        {
            t_float msec = list[ 0 ].a_w.w_float;
           
            ClipMin( msec, 1 );
           
            object->pixel_msec = msec / fontmap_x_px;
        }
        else
        {
            pd_error( object, "teletype~: invalid msec argument type" );
        }
       
        if( items > 1 )
        {
            pd_error( object, "teletype~: extra msec arguments ignored" );
        }
    }
   
    if( StringMatch( message, "freq" ) )
    {
        if( items < 1 )
        {
            pd_error( object, "teletype~: no freq argument" );
            return;
        }
       
        if( list[ 0 ].a_type == A_FLOAT )
        {
            t_float freq = list[ 0 ].a_w.w_float;
           
            ClipMin( freq, 1 );
           
            object->freq = freq;
        }
        else
        {
            pd_error( object, "teletype~: invalid freq argument type" );
        }
       
        if( items > 1 )
        {
            pd_error( object, "teletype~: extra freq arguments ignored" );
        }
    }
}


//------------------------------------------------------------------------------
// teletype_perform - the signal processing function of this object
//------------------------------------------------------------------------------
static t_int* teletype_perform( t_int* io )
{
    // store variables from dsp input/output array
    t_float*    in1    = ( t_float*    )( io[ 1 ] );
    t_float*    out1   = ( t_float*    )( io[ 2 ] );
    t_float*    out2   = ( t_float*    )( io[ 3 ] );
    t_int       frames = ( t_int       )( io[ 4 ] );
    t_teletype* object = ( t_teletype* )( io[ 5 ] );
   
    // store pointers locally
    t_float* wavetable      = object->wavetable;
    t_float* raw            = object->raw;
    t_float* saw            = object->saw;
    t_float* amp            = object->amp;
    t_float* amp_smooth     = object->amp_smooth;
    t_float* amp_smooth_inc = object->amp_smooth_inc;
    t_float* phase          = object->phase;
    t_float* phase_inc      = object->phase_inc;
    t_float* fontmap        = object->fontmap;
   
    t_float smooth_coeff = 1.0 / frames;
   
    // calculate samples per pixel based
    t_float samps_per_pixel = object->pixel_msec * 0.001 * object->sample_rate;
   
    // wavetable & mask value
    long wavetable_mask = WAVETABLE_MASK;
   
    // calculate fundamental phase increment
    t_float fund_phase_inc = object->freq * ( 1.0 / object->sample_rate ) * WAVETABLE_SIZE;

    // iterator variables
    t_int n = -1;
    t_int o = -1;
   
    // clear output array
    memset( raw, 0, object->vector_bytes );
    memset( saw, 0, object->vector_bytes );
   
    // oscillator bank
    //--------------------------------------------------------------------------
   
    // if a character is ready to print
    if( object->print_char )
    {
        // iterate through the oscillators
        while( ( ++o < OSCILLATORS ) && ( object->print_char == TRUE ) )
        {
            // set target amplitude based on fontmap pixel values
            amp[ o ] = fontmap[ o * (int)fontmap_x_px + object->char_offset + object->x_pixel ];
           
            // calculate amplitude smoothing increments
            amp_smooth_inc[ o ] = ( amp[ o ] - amp_smooth[ o ] ) * smooth_coeff;
           
            // calculate phase increment for each partial
            phase_inc[ o ] = fund_phase_inc * ( OSCILLATORS - o );
       
            // reset vector iterator
            n = -1;
           
            // iterate through the signal vector
            while( ++n < frames )
            {
                // calculate fontmap variables
                if( o == 0 )
                {
                    // increment samples per pixel count
                    object->pixel_samps++;
                   
                    // if pixel samples have elapsed
                    if( object->pixel_samps >= samps_per_pixel )
                    {
                        // reset samples per pixel count
                        object->pixel_samps = 0;
                       
                        // increment pixel count
                        object->x_pixel++;
                       
                        // if pixel count is done
                        if( object->x_pixel >= fontmap_x_px )
                        {
                            // reset pixel count
                            object->x_pixel = 0;
                           
                            // stop printing the character
                            object->print_char = FALSE;
                           
                            // end synthesis
                            break;
                        }
                    }
                }
               
                // accumulate waveforms into waveform arrays
                raw[ n ] += wavetable[ ( unsigned long )phase[ o ] ] * amp_smooth[ o ];
                saw[ n ] += wavetable[ ( unsigned long )phase[ o ] ] * amp_smooth[ o ] * ( 1.0 / ( ( float )( OSCILLATORS - o ) / 3.0 ) );
               
                // increment phases
                phase[ o ] += phase_inc[ o ];
               
                // & wrap phases while preserving decimal values
                phase[ o ] = ( ( long ) phase[ o ] & wavetable_mask ) + ( phase[ o ] - ( long )phase[ o ] );

                // increment amplitudes
                amp_smooth[ o ] += amp_smooth_inc[ o ];
            }
           
            // set smooth amplitude to actual amplitude
            amp_smooth[ o ] = amp[ o ];
        }
   
    }
       
    // copy waveforms to output buffers
    memcpy( out1, raw, object->vector_bytes );
    memcpy( out2, saw, object->vector_bytes );
   
    // return the dsp input/output array address plus one more than its size
    // to provide a pointer to the next perform function in pd's call list
    return &( io[ 6 ] );
}


//------------------------------------------------------------------------------
// teletype_dsp - installs this object's dsp function in pd's callback list
//------------------------------------------------------------------------------
static void teletype_dsp( t_teletype* object, t_signal **sig )
{
    // store sample rate
    object->sample_rate = sig[ 0 ]->s_sr;
   
    // calculate size of vector
    object->vector_bytes = sig[ 0 ]->s_n * sizeof( t_float );
   
    // reallocate memory
    object->raw            = ( t_float* )realloc( object->raw,            object->vector_bytes );
    object->saw            = ( t_float* )realloc( object->saw,            object->vector_bytes );
    object->amp            = ( t_float* )realloc( object->amp,            object->vector_bytes );
    object->amp_smooth     = ( t_float* )realloc( object->amp_smooth,     object->vector_bytes );
    object->amp_smooth_inc = ( t_float* )realloc( object->amp_smooth_inc, object->vector_bytes );
   
    // clear memory
    memset( object->raw,            0, object->vector_bytes );
    memset( object->saw,            0, object->vector_bytes );
    memset( object->amp,            0, object->vector_bytes );
    memset( object->amp_smooth,     0, object->vector_bytes );
    memset( object->amp_smooth_inc, 0, object->vector_bytes );
   
    // dsp_add arguments
    //--------------------------------------------------------------------------
    // perform routine
    // number of passed parameters
    // inlet 1 sample vector
    // inlet 2 sample vector
    // outlet sample vector
    // sample frames to process (vector size)
    dsp_add( teletype_perform, 5, sig[ 0 ]->s_vec, sig[ 1 ]->s_vec, sig[ 2 ]->s_vec, sig[ 0 ]->s_n, object );
}


//------------------------------------------------------------------------------
// teletype_new - instantiates a copy of this object in pd
//------------------------------------------------------------------------------
static void* teletype_new( t_symbol *s, t_int argc, t_atom *argv )
{
    // create a pointer to this object
    t_teletype* object = ( t_teletype* )pd_new( teletype_class );
   
    // create signal outlets for this object
    outlet_new( &object->object, gensym( "signal" ) );
    outlet_new( &object->object, gensym( "signal" ) );
   
    // initialize the value of inlet_1 variable
    object->inlet_1 = 0;

    // initialize variables
    object->freq         = 300;
    object->phase        = 0;
    object->phase_inc    = 0;
    object->vector_bytes = 0;
    object->sample_rate  = 0;
   
    // initialize pointers
    object->wavetable      = NULL;
    object->raw            = NULL;
    object->saw            = NULL;
    object->amp            = NULL;
    object->amp_smooth     = NULL;
    object->amp_smooth_inc = NULL;
    object->phase          = NULL;
    object->phase_inc      = NULL;
    object->fontmap        = NULL;
   
    // allocate memory
    object->wavetable = ( t_float* )calloc( WAVETABLE_SIZE, sizeof( t_float ) );
    object->phase     = ( t_float* )calloc( OSCILLATORS,    sizeof( t_float ) );
    object->phase_inc = ( t_float* )calloc( OSCILLATORS,    sizeof( t_float ) );
    object->fontmap   = ( t_float* )calloc( fontmap_size,   sizeof( t_float ) );
   
    // fill the fontmap
    memcpy( object->fontmap, fontmap_init, fontmap_size * sizeof( t_float ) );
   
    // temporary wavetable calculation variables
    long    i = -1;
    t_float x;
   
    // fill the wavetable with a single sine wave cycle
    while( ++i < WAVETABLE_SIZE )
    {
        x = ( float )i / ( float )WAVETABLE_SIZE;
       
        object->wavetable[ i ] = Sine( C_2_PI * x );
    }

    return object;
}


//------------------------------------------------------------------------------
// teletype_free - cleans up memory allocated by this object
//------------------------------------------------------------------------------
static void teletype_free( t_teletype* object )
{
    // if memory is allocated
    if( object->wavetable )
    {
        // deallocate the memory
        free( object->wavetable );
       
        // set the memory pointer to null
        object->wavetable = NULL;
    }
   
    // . . .
    if( object->raw )
    {
        free( object->raw );
        object->raw = NULL;
    }

    if( object->saw )
    {
        free( object->saw );
        object->saw = NULL;
    }
   
    if( object->amp )
    {
        free( object->amp );
        object->amp = NULL;
    }
   
    if( object->amp_smooth )
    {
        free( object->amp_smooth );
        object->amp_smooth = NULL;
    }
   
    if( object->amp_smooth_inc )
    {
        free( object->amp_smooth_inc );
        object->amp_smooth_inc = NULL;
    }
   
    if( object->phase )
    {
        free( object->phase );
        object->phase = NULL;
    }
   
    if( object->phase_inc )
    {
        free( object->phase_inc );
        object->phase_inc = NULL;
    }
   
    if( object->fontmap )
    {
        free( object->fontmap );
        object->fontmap = NULL;
    }
}


//------------------------------------------------------------------------------
// teletype_tilde_setup - describes the attributes of this object to pd so it may be properly instantiated
// (must always be named with _tilde replacing ~ in the object name)
//------------------------------------------------------------------------------
void teletype_tilde_setup( void )
{
    // teletype class
    //--------------------------------------------------------------------------
   
    // creates an instance of this object and describes it to pd
    teletype_class = class_new( gensym( "teletype~" ), ( t_newmethod )teletype_new, ( t_method )teletype_free, sizeof( t_teletype ), 0, A_GIMME, 0 );
   
    // declares leftmost inlet as a signal inlet
    CLASS_MAINSIGNALIN( teletype_class, t_teletype, inlet_1 );
   
    // installs teletype_dsp so that it will be called when dsp is turned on
    class_addmethod( teletype_class, ( t_method )teletype_dsp, gensym( "dsp" ), 0 );
   
    // add float handler
    class_addfloat( teletype_class, teletype_float );
   
    // add message handler that responds to any message
    class_addmethod( teletype_class, ( t_method )teletype_message, gensym( "anything" ), A_GIMME, 0 );

    // announce this object in the pd console
    Announce( "teletype~: spectral ascii printer - v1.0 - Cooper Baker" );
}


//------------------------------------------------------------------------------
// EOF
//------------------------------------------------------------------------------
 
asciibuf.c
//------------------------------------------------------------------------------
//  Pd Spectral Toolkit
//
//  asciibuf.c
//
//  Accepts mixed float / symbol input and sorts it to corresponding outlets
//
//  Created by Cooper Baker on 3/29/12.
//  Copyright (c) 2012 Cooper Baker. All rights reserved.
//------------------------------------------------------------------------------


//------------------------------------------------------------------------------
// m_pd.h - main header for Pd
//------------------------------------------------------------------------------
#include "m_pd.h"

// utility header for Pd Spectral Toolkit project
#include "utility.h"


//------------------------------------------------------------------------------
// asciibuf_class - pointer to this object's definition
//------------------------------------------------------------------------------
t_class* asciibuf_class;


//------------------------------------------------------------------------------
// asciibuf - data structure holding this object's data
//------------------------------------------------------------------------------
typedef struct asciibuf
{
    // this object - must always be first variable in struct
    t_object object;
   
    // pointers to the outlets
    t_outlet* outlet;
   
    // pointer to float list
    t_float* ascii;
   
    // character count
    unsigned long chars;

} t_asciibuf;


//------------------------------------------------------------------------------
// function prototypes
//------------------------------------------------------------------------------
void  asciibuf_message ( t_asciibuf* object, t_symbol* selector, t_int items, t_atom* list );
void  asciibuf_bang    ( t_asciibuf* object );
void* asciibuf_new     ( void );
void  asciibuf_free    ( t_asciibuf* object );
void  asciibuf_setup   ( void );


//------------------------------------------------------------------------------
// asciibuf_bang - shift out a float ( fifo )
//------------------------------------------------------------------------------
void asciibuf_bang( t_asciibuf* object )
{
    if( object->chars )
    {
        // output first ascii value in list
        outlet_float( object->outlet, object->ascii[ 0 ] );
       
        // decrement character count
        object->chars--;
       
        // shift the ascii buffer
        memmove( object->ascii, &( object->ascii[ 1 ] ), object->chars * sizeof( t_float ) );

        // trim the ascii buffer
        object->ascii = ( t_float* )realloc( object->ascii, object->chars * sizeof( t_float ) );
    }
}


//------------------------------------------------------------------------------
// asciibuf_parse - parses list input
//------------------------------------------------------------------------------
void asciibuf_message( t_asciibuf* object, t_symbol* selector, t_int items, t_atom* list )
{
    // iterator for input list
    t_int i = 0;
   
    // local ascii character count
    unsigned long a = 0;
   

    // check for a bad selector
    if ( ( StringMatch( selector->s_name, "float" ) == TRUE ) && ( items == 1 ) )
    {
        ; // do nothing for single floats
    }
    else if( StringMatch( selector->s_name, "list" ) == FALSE )
    {
        // display an error
        pd_error( object, "asciibuf: unknown selector" );
    }
   
    // iterate through the input messages
    for( i = 0 ; i < items ; i++ )
    {
        // if the input message is a float
        if( list[ i ].a_type == A_FLOAT )
        {
            // increment the ascii count
            a++;
        }
        // if the input message is a symbol
        else if( list[ i ].a_type == A_SYMBOL )
        {
            // display an error for bad input
            pd_error( object, "asciibuf: non-float list items ignored" );
        }
    }
   
    // allocate more memory for the incoming values
    object->ascii = ( t_float* )realloc( object->ascii, ( object->chars + a ) * sizeof( t_float ) );
   
    // reset local ascii character count
    a = 0;
   
    // iterate through the input messages
    for( i = 0 ; i < items ; i++ )
    {
        // if the input message is a float
        if( list[ i ].a_type == A_FLOAT )
        {
            // save the float into ascii list
            object->ascii[ object->chars + a ] = list[ i ].a_w.w_float;

            // increment the ascii count
            a++;
        }
    }
   
    // update character count
    object->chars += a;
}


//------------------------------------------------------------------------------
// asciibuf_new - initialize the object upon instantiation ( aka "constructor" )
//------------------------------------------------------------------------------
void* asciibuf_new( void )
{
    // declare a pointer to this class
    t_asciibuf* object;
   
    // generate a new object and save its pointer in "object"
    object = ( t_asciibuf* )pd_new( asciibuf_class );
   
    // generate two new outlets and save their pointers in the object's struct
    object->outlet  = outlet_new( &object->object, gensym( "float" )  );
   
    // initialize pointer
    object->ascii = NULL;
   
    // initialize character count
    object->chars = 0;
   
    // return the pointer to this class
    return ( void* )object;
}


//------------------------------------------------------------------------------
// teletype_free - cleans up memory allocated by this object
//------------------------------------------------------------------------------
void asciibuf_free( t_asciibuf* object )
{
    // clean up the float list
    if( object->ascii )
    {
        free( object->ascii );
        object->ascii = NULL;
    }
}


//------------------------------------------------------------------------------
// asciibuf setup - defines this object and its properties to Pd
//------------------------------------------------------------------------------
void asciibuf_setup( void )
{
    // create a new class and assign its pointer to asciibuf_class
    asciibuf_class = class_new( gensym( "asciibuf" ), ( t_newmethod )asciibuf_new, ( t_method )asciibuf_free, sizeof( t_asciibuf ), 0, 0 );
   
    // add message handler, responding to any message
    class_addmethod( asciibuf_class, ( t_method )asciibuf_message, gensym( "anything" ), A_GIMME, 0 );
   
    // add bang handler
    class_addbang( asciibuf_class, ( t_method )asciibuf_bang );
   
    // announce this object in the pd console
    Announce( "asciibuf: buffers strings of ascii values - v1.0" );
}


//------------------------------------------------------------------------------
// EOF
//------------------------------------------------------------------------------